/*
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.chain;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import tal.drivechain.chain.shell.InfoPrinter;
import tal.drivechain.module.properties.PropertyParserFactory;
import tal.drivechain.module.xmlconfig.PropertyInfo;
import tal.drivechain.plugin.Channel;
import tal.drivechain.plugin.Logger;
import tal.drivechain.plugin.Module;
import tal.drivechain.plugin.ModulePackageManager;
import tal.drivechain.system.Globals;
import tal.drivechain.system.Version;
import tal.drivechain.variable.SetProperty;
import tal.drivechain.variable.types.DataType;
import tal.drivechain.variable.SetProperty.Flag;
import tal.drivechain.variable.VarTableAction;
import tal.drivechain.variable.VarTableListener;
import tal.drivechain.variable.Variable;

/**
 *
 * @author eisental
 */
public class Chain extends ChainNode {
    private int freeNodeIndex = 0;

    private ConcurrentHashMap<String,ChainNode> nodes = new ConcurrentHashMap<String,ChainNode>();
    private HashMap<ChainNode,NodeExport> exports = new HashMap<ChainNode,NodeExport>();
    protected HashMap sharedObjects = new HashMap();

    protected List<ChainListener> chainListeners = new ArrayList<ChainListener>();
    protected Patcher patcher;
    protected File scriptFilePath;
    protected List<Module> inTerminals = new ArrayList<Module>();
    protected List<Module> outTerminals = new ArrayList<Module>();
    private OutReceiver receiver = new OutReceiver();

    public boolean isLoadedFromFile = false;

    public static Chain createRootChain() {
        Chain c = new Chain();
        c.nodeName = "root";
        return c;
    }

    public Chain() {
        nodeInfo = new ChainInfo();
        patcher = new FreePatcher();
        this.getVarTable().addListener(new VarTableListener() {

            public void globalMessageEvent(Variable message) {}

            public void setVarEvent(Variable message) {}

            public void removeVarEvent(Variable var) {}

            public void varScopeChanged(Variable var, boolean isPublic) {
                if (isPublic) {
                    nodeInfo.getProperties().add(makeVarProperty(var));
                } else {
                    PropertyInfo removeInfo = null;
                    for (PropertyInfo i : nodeInfo.getProperties()) {
                        if (i.getName().equals(var.getName())) { removeInfo = i; break; }
                    }

                    if (removeInfo!=null) nodeInfo.getProperties().remove(removeInfo);
                }
            }
        });
    }

    public Chain(String name) {
        this();
        setName(name);
    }

    public void setPatcher(Patcher patcher) { this.patcher = patcher; }

    public void patchNodes(Patcher patcher) throws AbortChainException {
        this.patcher = patcher;
        patcher.patchNodes(this);
    }

    public void addChild(ChainNode node) {
        if (node==null) return;

        if (node.getName()==null) {
            node.setName(makeUniqueNodeName(node));
        }

        for (String name : nodes.keySet())
            if (node.getName().equals(name))
                throw new IllegalArgumentException("Node named " + node.getName() + " already exists in chain " + getPath());

        node.setParentChain(this);
        nodes.put(node.getName(), node);
        if (node.getNodeClassName().equals("InTerminal")) inTerminals.add((Module)node);
        else if (node.getNodeClassName().equals("OutTerminal")) {
            // add every output the chain has to the outTerminal.
            //System.out.println("adding output to outTerminal " + node.getName());
            outTerminals.add((Module)node);
            node.addOutput(receiver);
            //System.out.println("node " + node.getPath() + " has now " + node.getOutputs().size() + " output(s).");
        }

        node.setDebugFlag(debugFlag);
        node.setInfoFlag(infoFlag);
        for (Logger l : loggers)
            node.addLogger(l);

        //System.out.println("Added child " + node.getName() + " (" + node.getNodeClassName() + ") ");
        //System.out.println("Loggers: ");
        //for (Logger l : loggers) System.out.println(l.getClass().getName());
        
        for (ChainListener l : chainListeners) l.childAdded(this, node);

    }

    public void renameChild(ChainNode child, String newName) {
        if (!ChainNode.isNodeNameLegal(newName))
            throw new IllegalArgumentException("Illegal node name: " + newName);

        if (nodes.remove(child.getName())==null)
            throw new IllegalArgumentException("Node " + child.getName() + " is not a child node of chain " + getPath());
        nodes.put(newName, child);
        child.setName(newName);
    }

    public void renameChild(String nodeName, String newName) {
        if (nodes.containsKey(nodeName)) {
            ChainNode n = nodes.get(nodeName);
            renameChild(n, newName);
        } else throw new IllegalArgumentException("Node " + nodeName + " is not a child of chain " + getPath());
    }

    public ChainNode removeChild(String nodeName) {
        if (nodes.containsKey(nodeName)) {
            ChainNode node = nodes.get(nodeName);
            if (node instanceof Module) {
                if (inTerminals.contains((Module)node))
                    inTerminals.remove((Module)node);
                else if (outTerminals.contains((Module)node))
                    outTerminals.remove((Module)node);
            }

            ChainNode removedNode = nodes.remove(nodeName);
            for (ChainListener l : chainListeners) l.childRemoved(this, node);
            return removedNode;
        } else throw new IllegalArgumentException("Node " + nodeName + " is not contained within " + this.getPath());
    }

    @Override
    public void reset() {
        this.exports.clear();
        this.inTerminals.clear();
        this.outTerminals.clear();

        for (ChainNode n : nodes.values()) { n.destroy(); }
        super.reset();
    }

    @Override
    public void destroy() {
        List<ChainNode> children = new ArrayList<ChainNode>();
        for (ChainNode n : nodes.values()) children.add(n);
        for (ChainNode n : children) n.destroy();

        super.destroy();
    }

    @Override
    public void send(Variable in, long timestamp, Channel channel) {
        super.send(in, timestamp, channel);
        if (debugFlag) log("Sending " + in.toString(this) + " (" + timestamp + ") to input terminals (InTerminal).", MSG.DEBUG);

        if (channel==Channel.CHAIN) {
            if (in.getDataType().matches(DataType.PROPERTY) || in.getDataType().matches(DataType.PROPERTY, DataType.MATCH_ARRAY)) {
                if (in.getDataType().matches(DataType.PROPERTY) && channel == Channel.CHAIN) {
                    SetProperty prop = (SetProperty)in.getValue(this);
                    if (debugFlag)
                        log("Received a prop: " + prop.toString(), MSG.DEBUG);
                    prop.invokeSetProperty(this);
                } else if (in.getDataType().matches(DataType.PROPERTY, DataType.MATCH_ARRAY) && channel == Channel.CHAIN) {
                    List<Object> internal = (List<Object>)in.getInternalValue();
                    if (debugFlag)
                        log("Received a prop array: " + in.toString(this), MSG.DEBUG);
                    for (Object val : internal) {
                        ((SetProperty)val).invokeSetProperty(this);
                    }
                }
            } else {
                for (Module module : inTerminals)
                    module.send(in, timestamp, channel);
            }
        }

    }

    public String generateScript() {
        if (isLoadedFromFile && this.scriptFilePath!=null) return generateLoadChainScript() + " " + generateConnections();

        String script = "";
        try {
            String meta = generateMetaDataScript();
                if (meta.trim().length()!=0) meta += "\n";
            String vars = localVars.generateScript();
                if (vars.trim().length()!=0) vars += "\n";
            String exps = generateModuleExportsScript();
                if (exps.trim().length()!=0) exps += "\n";
            String ns = generateNodesScript();
            String hiddenMeta = generateHiddenMetaDataScript();
                if (hiddenMeta.trim().length()!=0) hiddenMeta += "\n";
            
            String chainDump = meta + vars + exps + ns + hiddenMeta;
            if (chainDump.trim().length()==0) script = "chain " + getName() + ";";
            else script = "chain " + getName() + " {\n\t" + chainDump.trim().replaceAll("\\n", "\n\t") + "\n}; ";

            return (script + generateConnections()).trim();
        } catch (Exception ex) {
            log("Error while generating script for " + getPath() + ": " + ex.getMessage(), MSG.ERROR);
            log(script, MSG.ERROR);
            ex.printStackTrace();
            throw new IllegalArgumentException(ex.getMessage());
        }


    }

    private String generateNodesScript() {
        String script = "";
        for (ChainNode n : this.nodes.values()) {
            script += n.generateScript() + "\n\n";
        }

        //if (script.trim().length()>0) script += "\n";
        return script;
    }

    private String generateModuleExportsScript() {
        String ret = "";
        for (NodeExport exp : exports.values()) {
            ret += exp.generateScript() + "\n";
        }
        if (ret.trim().length()>0) return ret + "\n";
        else return "";
    }

    private String generateLoadChainScript() {
        String ret = "chain " + getName() + " <" + findRelativePath(scriptFilePath, getParentChain().getScriptFilePath()) + ">";
        if (nodeInfo.getProperties().isEmpty()) return ret + ";";
        else {
            String props = " {\n";
            for (PropertyInfo p : nodeInfo.getProperties()) {
                props += "\t" + InfoPrinter.printProperty(this, p, getProperty(p.getName())) + ";\n";
            }
            return ret + props + "};";
        }
    }

    private String generateConnections() {
        String script = "";
        if (this.getOutputs().size()>0) {
            script += "connect " + getName() + " to ";
            for (ChainNode out : getOutputs())
                script += out.getName() + ", ";
            script = script.substring(0, script.length()-2) + ";";
        }
        return script;
    }

    public HashMap getSharedObjects() { return sharedObjects; }

    /**
     * Try to find a shared object by looking in this chain and any of its parents.
     * @param id A path to the shared object. For example: parent.parent.parent.someid will look in the chains 3rd parent for a shared object named 'someid'
     * @return the shared object.
     * @throws java.lang.IllegalArgumentException If the parent doesn't exist or the shared object is not found.
     */
    public Object findSharedObject(String id) throws IllegalArgumentException {
        if (id.startsWith("parent.")) {
            if (this.getParentChain()!=null)
                return this.getParentChain().findSharedObject(id.substring(7));
            else throw new IllegalArgumentException("Chain " + getPath() + "doesn't have a parent chain.");
        } else {
            Object ret = sharedObjects.get(id);
            if (ret==null)
                throw new IllegalArgumentException("Shared object '" + id + "' doesn't exist in chain " + getPath());
            else return ret;
        }
    }

    /**
     * Adds a property export list for a specific module to this chain.
     * @param e the module property export list.
     */
    public void addNodeExport(NodeExport e) {
        // if we already have an export to this node remove it from nodeInfo before putting new export.
        ChainNode node = e.getExportedNode();
        if (exports.containsKey(node)) {
            this.removeNodeExport(node);
        }

        exports.put(node, e);

        for (String prop : e.getProperties()) {
            HashMap<String,String> parserAttr = new HashMap<String,String>();
            parserAttr.put("requires-apostrophes", "true");
            PropertyInfo i = node.getPropertyInfo(prop);
            nodeInfo.getProperties().add(PropertyInfo.createPropertyInfo(node.getName() + "." + prop, "", i.getParser(), i.getAttributes()));
        }

    }

    public void removeNodeExport(ChainNode node) {
        NodeExport n = exports.get(node);

        if (n==null)
            throw new IllegalArgumentException("Exports for module " + node.getName() + " do not exist in chain " + this.getPath());
        
        for (String prop : n.getProperties())
            nodeInfo.properties.remove(nodeInfo.findPropertyInfo(node.getName() + "." + prop));

        exports.remove(node);
    }

    public Map<ChainNode, NodeExport> getNodeExports() { return Collections.unmodifiableMap(exports); }

    public ChainNode getChild(String nodeName) { 
        return nodes.get(nodeName);
    }

    public ChainNode getNodeByPath(String nodePath) {
        return Globals.findNode(nodePath, this);
    }

    public Map<String, ChainNode> getChildNodes() {
        return Collections.unmodifiableMap(nodes);
    }

    public void dispatchNodeConfigured(ChainNode node) {
        for (ChainListener l : chainListeners) l.nodeConfigured(this, node);
    }

    public void addChainListener(ChainListener listener) {
        System.out.println("Adding chain listener: " + ((listener instanceof ChainNode)?((ChainNode)listener).getName():listener));
        if (!chainListeners.contains(listener))
            chainListeners.add(listener);
        System.out.println("listeners: " + chainListeners);
    }

    public void removeChainListener(ChainListener listener) {
        System.out.println("Removing chain listener: " + ((listener instanceof ChainNode)?((ChainNode)listener).getName():listener));
        chainListeners.remove(listener);
    }

    public void setScriptFilePath(File path) { this.scriptFilePath = path; }
    public File getScriptFilePath() { return this.scriptFilePath; }

    /**
     * Adds default data to the chains metadata table.
     * Currently added is:
     * depends-on - A comma delimited list of versions of package that are used in this chain.
     */
    public void addDefaultMeta() {
        this.putMetadata(".depends-on", listDependencies());
    }


    /**
     * Calculate the required packages to run this chain by checking the source package of
     * each of the chain modules.
     * @return
     */
    private String listDependencies() {
        String ret = Globals.VERSION.getSrcName() + "/[" + Globals.VERSION.toString(false, true, true, true) + "..], ";
        List<ModulePackageManager> p = new ArrayList<ModulePackageManager>();
        for (ChainNode n : nodes.values()) {
            if (n instanceof Module) {
                Module m = (Module)n;
                if (!p.contains(m.getPackageManager()))
                    p.add(m.getPackageManager());
            }
        }
        for (ModulePackageManager man : p) {
            Version ver = man.getPackageConfiguration().getPackageInfo().getVersion();
            ret += ver.getSrcName() + "/[" + man.getPackageConfiguration().getPackageInfo().getVersion().toString(false, true, true, true) + "..], ";
        }
        return ret.substring(0, ret.length()-2);
    }

    @Override
    public void start() {
        if (isRunning()) return;

        ChainListener aborter = null;
        for (ChainListener l : chainListeners)
            if (!l.chainStarting(this)) { aborter = l; break; }
        if (aborter != null) {
            throw new IllegalArgumentException(aborter.toString() + "(" + aborter.getClass().getName() + ") aborted starting chain " + getPath() + ". ");
        }

        for (ChainNode node : nodes.values()) {
            try {
                node.start();
            } catch (Exception e) {
                e.printStackTrace();
                throw new IllegalArgumentException("While opening " + node.getName() + ": " + e);
            }
        }

        if (debugFlag) log("Chain '" + this.getPath() + "' is now running.", Module.MSG.INFO);
        super.start();
        getVarTable().sendGlobalMessage(new Variable("init", DataType.EVENT, true, true));
    }

    /**
     * Calls the stop() method of every Module object and children in the chain.
     * Allows a chain listener to abort stopping the chain and sends an exit event variable to its modules and nested chains.
     */
    @Override
    public void stop() {
        if (!isRunning()) return;

        getVarTable().sendGlobalMessage(new Variable("exit", DataType.EVENT, true, true));
        
        ChainListener aborter = null;
        for (ChainListener l : chainListeners)
            if (!l.chainStopping(this)) aborter = l;
        if (aborter != null) {
            log(aborter.toString() + "(" + aborter.getClass().getName() + ") aborted chain shutdown.", Module.MSG.ANOUNCE);
            return;
        }

        for (ChainNode node : nodes.values()) {
            node.stop();
        }

        log("Chain " + this.getName() + " is closed.", Module.MSG.INFO);
        super.stop();
        for (ChainListener l : chainListeners)
            l.chainStopped(this);
    }

    /**
     * Sets whether the chain should send info messages to its loggers.
     * This is done recursively on any module or chain contained within the chain.
     *
     * @param info The new value of info flag.
     */
    @Override
    public void setInfoFlag(boolean info) {
        super.setInfoFlag(info);
        for (ChainNode n : nodes.values()) n.setInfoFlag(info);
    }

    /**
     * Sets whether the chain should send debug messages to its loggers.
     * This is done recursively on any module or chain contained within the chain.
     *
     * @param debug The new value of debug flag.
     */
    @Override
    public void setDebugFlag(boolean debug) {
        super.setDebugFlag(debug);
        for (ChainNode n : nodes.values()) n.setDebugFlag(debug);
    }

    @Override
    public void addLogger(Logger l) {
        super.addLogger(l);
        for (ChainNode n : nodes.values()) n.addLogger(l);
    }

    @Override
    public void removeLogger(Logger l) {
        super.removeLogger(l);
        for (ChainNode n : nodes.values()) n.removeLogger(l);
    }

    public Patcher getPatcher() {
        return patcher;
    }

    public void storeChainVar(String name, Variable var) {
        if (getVarTable().containsKey(name)) throw new IllegalArgumentException("Variable " + name + " is already defined in chain " + getPath());

        try {
//            System.out.println("storing chain " + var.getName() + " var ~=" + var.isEvaluateAtHome());
            if (var.isEvaluateAtHome())
                var = var.evaluate(this);
            VarTableAction action = VarTableAction.parseAction(VarTableAction.Action.SET_ANY, name);
            log("Storing private chain variable: " + var, Module.MSG.INFO);
            action.set(var, getVarTable(), this);
//            System.out.println("storing chain " + var.toString());
        } catch (IllegalArgumentException ie) {
            log("Error while storing " + var.getName() + ": " + ie.getMessage(), Module.MSG.ERROR);
        }


    }

    public void storePublicVar(String name, Variable var) {
        this.storeChainVar(name, var);
        getVarTable().setPublicVar(name, true);
    }



    private static PropertyInfo makeVarProperty(Variable var) {
        HashMap<String,String> parserAttr = new HashMap<String,String>();
        parserAttr.put("parse-method", "value");
        parserAttr.put("default-name", var.getName());
        parserAttr.put("default-type",var.getDataType().getName());
        parserAttr.put("enforce-types", var.getDataType().getName());
        return PropertyInfo.createPropertyInfo(var.getName(), "", PropertyParserFactory.createParser("var-parser", parserAttr, null));
    }

    @Override
    public void setupProperties() throws IllegalArgumentException, AbortChainException {
        for (ChainNode node : nodes.values()) {
            try {
                if (node instanceof Module) {
                    node.setupProperties();
                    if (node.getParentChain()!=null) node.getParentChain().dispatchNodeConfigured(node);
                }
            } catch (IllegalArgumentException ie) {
                for (Logger l : loggers)
                    l.nodeMessage(ie.getMessage(), Module.MSG.ERROR, node);
                throw ie;
            }
        }

        if (this.getParentChain()!=null) this.getParentChain().dispatchNodeConfigured(this);
    }

    @Override
    public String getNodeClassName() {
        return "Chain";
    }

    @Override
    public Object getProperty(String propName) {
        int firstDot = propName.indexOf('.');
        if (firstDot!=-1) { // set exported node property
            String nodename = propName.substring(0, firstDot);
            ChainNode node = nodes.get(nodename);
            if (node==null) throw new IllegalArgumentException("Unknown property " + propName + " in chain " + getPath());
            NodeExport exp = this.exports.get(node);
            if (exp==null) throw new IllegalArgumentException("Property " + propName + " is not exported by node " + node.getName());
            return exp.getProperty(propName.substring(firstDot+1));
        } else { // get public variable
            Variable v = this.findVariable(propName);
            if (v==null) throw new IllegalArgumentException("Public variable " + propName + " was not found.");
            else if (!this.getVarTable().isPublicVar(v.getName())) throw new IllegalArgumentException("Variable " + v.getName() + " is not public in chain " + getPath());
            else return v;
        }
    }

    @Override
    public void setProperty(String propName, Object newValue, Flag flag) {
        //System.out.println(getPath() + ">Set property " + propName + " val= " + newValue.toString());
        int firstDot = propName.indexOf('.');
        if (firstDot!=-1) { // set exported node property
            String nodename = propName.substring(0, firstDot);
            ChainNode node = nodes.get(nodename);
            if (node==null) throw new IllegalArgumentException("Unknown property " + propName + " in chain " + getPath());
            NodeExport exp = this.exports.get(node);
            if (exp==null) throw new IllegalArgumentException("Property " + propName + " is not exported by node " + node.getName());

//            System.out.println("setting property of node " + node.getPath() + " named " + propName + " value=" + newValue.toString() + "(" + newValue.getClass().getName() + ")");
            if (!(node instanceof Chain)) {
                String value = (String)newValue;
                try {
                    PropertyInfo p = this.getPropertyInfo(propName);
                    exp.setProperty(propName.substring(firstDot+1), p.getParser().parseProperty(value), flag);
                } catch (IllegalArgumentException ie) {
                    throw new IllegalArgumentException("Node " + node.getName() + ": " + ie);
                }
            } else {
                exp.setProperty(propName.substring(firstDot+1), newValue, flag);
            }
        } else { // set public variable
            VarTableAction setVar = VarTableAction.parseAction(VarTableAction.Action.SET_EXISTING, propName);
            Variable var = null;
            PropertyInfo p = this.getPropertyInfo(propName);
            if (p==null) throw new IllegalArgumentException("Unknown property " + propName + " in chain " + getPath());
            else var = (Variable)p.getParser().parseProperty((String)newValue);
            setVar.set(var, localVars, this);
        }
    }

    @Override
    public Object getOriginalPropValue(String name) {
        throw new IllegalArgumentException("A chain node doesn't keep track of its original property values.");
    }

    @Override
    public boolean isPropertyGettable(String propName) {
        int firstDot = propName.indexOf('.');
        if (firstDot==-1) {
            // should be a public var
            if (localVars.containsKey(propName) && localVars.isPublicVar(propName))
                return true;
            else return false;
        } else {
            // export
            String nodename = propName.substring(0, firstDot);
            ChainNode node = nodes.get(nodename);
            if (node==null) throw new IllegalArgumentException("Unknown property " + propName + " in chain " + getPath());
            NodeExport exp = this.exports.get(node);
            if (exp==null) throw new IllegalArgumentException("Property " + propName + " is not exported by node " + node.getName());
            return node.isPropertyGettable(propName.substring(firstDot+1));
        }
    }

    @Override
    public boolean isPropertySettable(String propName) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public static String findRelativePath(File path, File refPath) {
//        System.out.println("FINDRELATIVEPATH");
        File parent = path.getParentFile();
        File scriptsPath = new File(Globals.getScriptsPath());
        String curFileName = path.getName();
        while (parent!=null) {
//            System.out.println("curFileName=" + curFileName);
            if (parent.equals(refPath) || parent.equals(scriptsPath)) {
                return curFileName;
            } else {
                curFileName = parent.getName() + File.separator + curFileName;
                parent = parent.getParentFile();                
            }
        }

        return path.getAbsolutePath();
    }

    @Override
    public PropertyInfo getPropertyInfo(String prop) {
        for (PropertyInfo i : nodeInfo.getProperties())
            if (i.getName().equals(prop)) return i;

        throw new IllegalArgumentException("Unknown chain property: " + prop + " in chain " + getPath());
    }

    @Override
    public Variable findVariable(String name) {
            if (localVars!=null)
                return localVars.get(name);
            else return null;

    }

    private String makeUniqueNodeName(ChainNode node) {
        String base = node.getClass().getSimpleName().toLowerCase();
        if (nodes.containsKey(base) || !ChainNode.isNodeNameLegal(base)) {
            // try again and again to find a unique name.
            String unique = base + (freeNodeIndex++);
            while (nodes.containsKey(unique) || !ChainNode.isNodeNameLegal(unique)) {
                unique = base + (freeNodeIndex++);
            }
            return unique;
        } else return base;
    }

    public void dispatchChildRenamed(ChainNode node) {
        for (ChainListener l : chainListeners) l.childRenamed(this, node);
    }

    class OutReceiver extends Module {
        public OutReceiver() { }
        
        @Override
        public void setupProperties() throws IllegalArgumentException, AbortChainException {
        }

        @Override
        public void send(Variable in, long timestamp, Channel channel) {
//            System.out.println("Sending using OutReceiver");
            if (channel == Channel.CHAIN) {
                Chain.this.sendToOutputs(in, timestamp);
            }
        }

        @Override
        protected void msgReceived(Variable in, long timestamp, Channel channel) {
        }
    }

}
